package hu.myai.indicators.util;

import hu.myai.api.indicators.AbstractIndicator;
import hu.myai.indicators.pivot.LastHighIndicator;
import hu.myai.indicators.pivot.LastLowIndicator;
import hu.myai.model.IndicatorVO;
import hu.myai.model.PivotVO;
import hu.myai.model.TickVO;
import hu.myai.model.TrendLineVO;

import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * It is not a traditionally indicator. It evaluates the stock chart and
 * initialize fibonacci levels. It always return to null; fibonacci list is
 * available thru getFiboList()
 *
 * @author bozbalint
 *
 * */
public class FibonacciAgent extends AbstractIndicator {

    /**
     * The object used when logging debug,errors,warnings and info.
     * */
    private final Logger log = LoggerFactory.getLogger(this.getClass());

    private Double prevLow;
    private Double prevHigh;

    /**
     * tolerance of the deviation
     * */
    private int pct = 5;

    private List<PivotVO> pivotList = new ArrayList<PivotVO>();

    private List<Fibonacci> fiboList = new ArrayList<Fibonacci>();

    /**
     * @return the fiboList
     */
    public List<Fibonacci> getFiboList() {
        return fiboList;
    }

    public FibonacciAgent(Double parameter) {
        super(parameter);
    }

    private String lastLowID;
    private String lastHighID;
    @Override
    public void init() {
        lastLowID = register(new LastHighIndicator(parameter));
        lastHighID = register(new LastLowIndicator(parameter));
    }

    @Override
    protected IndicatorVO indicatorCalculation() {

        // I expect registered indicator
        IndicatorVO lastHigh = indicatorMap.get(lastHighID);
        IndicatorVO lastLow = indicatorMap.get(lastLowID);

        // initialize
        if ((lastHigh == null || !lastHigh.isChanged()) || (lastLow == null || !lastLow.isChanged())) {
            return null;
        } else if (prevLow == null) {
            prevLow = lastLow.getValue();
            return null;
        } else if (prevHigh == null) {
            prevHigh = lastHigh.getValue();
            return null;
        }

        // get the pivot tick
        TickVO lastPivot = tickHistory.get(parameter.intValue());

        // start to eliminate the trend line if a support became a resistance
        double top = lastPivot.getOpen() > lastPivot.getClose() ? lastPivot.getOpen() : lastPivot.getClose();
        double bottom = lastPivot.getOpen() < lastPivot.getClose() ? lastPivot.getOpen() : lastPivot.getClose();
        for (Fibonacci fibo : fiboList) {
            TrendLineVO support = fibo.getSupport(top, lastPivot.getTimestamp());
            if (support.equals(fibo.getResistance(bottom, lastPivot.getTimestamp())))
                fibo.isUseful(false);
        }

        // set the pivot list
        if (prevLow < lastLow.getValue()) {
            pivotList.add(0, new PivotVO(lastPivot, PivotVO.HIGHER_LOW));
        } else if (prevHigh > lastHigh.getValue()) {
            pivotList.add(0, new PivotVO(lastPivot, PivotVO.LOWER_HIGH));
        } else if (prevLow > lastLow.getValue()) {
            pivotList.add(0, new PivotVO(lastPivot, PivotVO.LOWER_LOW));
        } else if (prevHigh < lastHigh.getValue()) {
            pivotList.add(0, new PivotVO(lastPivot, PivotVO.HIGHER_HIGH));
        }

        // if no new information the pivot is "known"
        if (isPivotOnFibonacciLevel(lastPivot, pct)) {
            log.info("On Fibonacci: " + lastPivot);
            return null;
        }

        // set Fibonacci objects and add to fiboList
        if (pivotList.size() > 4) {
            fibonacciSetupScenarios();
        }

        // TODO clean up the not used fibonacci
        if (fiboList.size() > 3) {
            removeFibonacci();
        }

        prevHigh = lastHigh.getValue();
        prevLow = lastLow.getValue();

        return null;
    }

    /**
     * TODO Clean up fiboList
     * */
    private void removeFibonacci() {
        for (Fibonacci fibo : fiboList) {
            if (fibo.getScore() < 0)
                fiboList.remove(fibo);
        }
    }

    /**
     * Depend on the scenarios configure and add fibonacci
     * */
    private void fibonacciSetupScenarios() {
        // if the last pivot is a low
        if (pivotList.get(0).getType() < 0) {

            PivotVO low4 = pivotList.get(0);

            int highIndex = prevHighPivot(0);
            PivotVO high3 = pivotList.get(highIndex);

            int lowIndex = prevLowPivot(highIndex);
            PivotVO low2 = pivotList.get(lowIndex);

            highIndex = prevHighPivot(lowIndex);
            PivotVO high1 = pivotList.get(highIndex);

            /**
             * Scenario 1
             *
             * fibonacci from top-down: if the previous high is the first level
             * than next low should be on a fibonacci level<br>
             * ...__.....<br>
             * __./\.....<br>
             * /\/..\....<br>
             * ......\/__<br>
             * ..........<br>
             * */
            if (high3.getType() == PivotVO.HIGHER_HIGH || high1.getType() == PivotVO.HIGHER_HIGH) {
                log.debug("Scenario 1.".toString());

                addFiboListIfZigZag(high3.getTick(), high1.getTick(), low4.getTick(), Fibonacci.rowLevels[1]);
                addFiboListIfZigZag(high3.getTick(), high1.getTick(), low4.getTick(), Fibonacci.rowLevels[2]);
            } else

            /**
             * Scenario 2
             *
             * fibonacci from top-down: if the next low is on the first level
             * than high in between should be on a fibonacci level<br>
             * .__.......<br>
             * ./\.......<br>
             * /..\.__...<br>
             * ..__\/\...<br>
             * ........\.<br>
             * ..........<br>
             * */
            if (high1.getType() == PivotVO.HIGHER_HIGH && high3.getType() == PivotVO.LOWER_HIGH) {
                log.debug("Scenario 2.".toString());

                addFiboListIfZigZag(high1.getTick(), high3.getTick(), low2.getTick(), Fibonacci.rowLevels[1]);
                addFiboListIfZigZag(high1.getTick(), high3.getTick(), low2.getTick(), Fibonacci.rowLevels[2]);
            }
        }
        // if the last pivot is a high
        else {

            PivotVO high4 = pivotList.get(0);

            int highIndex = prevLowPivot(0);
            PivotVO low3 = pivotList.get(highIndex);

            int lowIndex = prevHighPivot(highIndex);
            PivotVO high2 = pivotList.get(lowIndex);

            highIndex = prevLowPivot(lowIndex);
            PivotVO low1 = pivotList.get(highIndex);

            /**
             * Scenario 3
             *
             * fibonacci from bottom-up: if the previous low is on the first
             * level than high should be on a fibonacci level<br>
             * ...........<br>
             * ........__.<br>
             * .\....../\.<br>
             * __\/\../...<br>
             * .....\/__..<br>
             * ...........<br>
             * */
            if (low1.getType() == PivotVO.LOWER_LOW && low3.getType() == PivotVO.LOWER_LOW) {
                log.debug("Scenario 3.".toString());

                addFiboListIfZigZag(low3.getTick(), low1.getTick(), high4.getTick(), Fibonacci.rowLevels[1]);
                addFiboListIfZigZag(low3.getTick(), low1.getTick(), high4.getTick(), Fibonacci.rowLevels[2]);
            } else

            /**
             * Scenario 4
             *
             * fibonacci from bottom-up: if the next low is on the first level
             * than high in between, should be on a fibonacci level<br>
             * ............<br>
             * ......__../.<br>
             * ...../\../..<br>
             * .\../..\/__.<br>
             * ..\/__......<br>
             * ............<br>
             * */
            if (low1.getType() == PivotVO.LOWER_LOW && low3.getType() == PivotVO.HIGHER_LOW) {
                log.debug("Scenario 4.".toString());

                addFiboListIfZigZag(low1.getTick(), low3.getTick(), high2.getTick(), Fibonacci.rowLevels[1]);
                addFiboListIfZigZag(low1.getTick(), low3.getTick(), high2.getTick(), Fibonacci.rowLevels[2]);
            }
        }
    }

    /**
     * @param tick
     *            the input tick
     * @param pct
     *            percent of the tolerance
     * @return if the tick touches a fibonacci level
     */
    private boolean isPivotOnFibonacciLevel(TickVO tick, int pct) {
        for (Fibonacci fibo : fiboList) {
            boolean isLevelHigh = fibo.isOnTrendLine(tick.getHigh(), tick.getTimestamp(), pct);
            boolean isLevelLow = fibo.isOnTrendLine(tick.getLow(), tick.getTimestamp(), pct);
            boolean isLevelPivot = fibo.isOnTrendLine(tick.getPivot(), tick.getTimestamp(), pct);
            if (isLevelHigh || isLevelLow || isLevelPivot) {
                return true;
            }
        }
        return false;
    }

    /**
     * @param pivotTick
     *            the low or the high turning tick (you assume it is fibonacci
     *            100%)
     * @param referenceTick
     *            the tick that you clarify as it should be on the first
     *            fibonacci level (you assume it is fibonacci 62.8%)
     * @param confirmationTick
     *            validating your assumption that the first 2 parameters are
     *            100% and 62.8% fibonacci levels
     * @param referencePct
     *            the % that we expect to do the first ZigZag. A value from
     *            Fibonacci.rowLevels (eg.: 0.236, 0.382)
     * @return result of your assumption. If the assumption was correct than it
     *         adds to fiboList
     */
    private boolean addFiboListIfZigZag(TickVO pivotTick, TickVO referenceTick, TickVO confirmationTick,
            double referencePct) {

        double confirmationDistance = pivotTick.getPivot() - confirmationTick.getPivot();
        double minMaxDistance = (pivotTick.getPivot() - referenceTick.getPivot()) / referencePct;
        // pct of the confirmation level
        double confirmationLevel = confirmationDistance / minMaxDistance;

        // check the assumption
        if (isFibLevel(confirmationLevel, pct)) {

            double pivotPair = pivotTick.getPivot() - minMaxDistance;
            // fibonacci cannot go negative
            if (pivotPair > 0) {
                fiboList.add(0, new Fibonacci(pivotTick.getPivot(), pivotPair, 0, 0));

                // log
                log.info(fiboList.get(0).toString());
                return true;
            }
        }
        return false;
    }

    /**
     * @param level
     *            value for check
     * @param pct
     *            the tolerance of deviation
     * @return if a value is on a fibonacci level
     */
    private boolean isFibLevel(double level, int pct) {
        for (int i = 1; i < Fibonacci.rowLevels.length; i++) {
            if (isInPct(Fibonacci.rowLevels[i] / level, pct)) {
                return true;
            }
        }
        return false;
    }

    private boolean isInPct(double value, int limit) {
        double absValue = Math.abs(value);
        return absValue < (1.0 + (limit / 100.0)) && absValue > (1.0 - (limit / 100.0));
    }

    /**
     * returns the previous high pivot. This method is necessary because
     * LastHigh indicator may skip a pivot and high and low pivots not
     * iterating.
     * */
    private int prevHighPivot(int index) {
        int i = index;
        for (; i < pivotList.size(); i++) {
            if (pivotList.get(i).getType() > 0)
                return i;
        }
        throw new ArrayIndexOutOfBoundsException("size:" + pivotList.size() + " i:" + i);
    }

    /**
     * returns the previous low pivot. This method is necessary because LastLow
     * indicator may skip a pivot and high and low pivots not iterating.
     * */
    private int prevLowPivot(int index) {
        int i = index;
        for (; i < pivotList.size(); i++) {
            if (pivotList.get(i).getType() < 0)
                return i;
        }
        throw new ArrayIndexOutOfBoundsException("size:" + pivotList.size() + " i:" + i);
    }

}
